import { useCall, useEffectRef, useForkRef, useMountedRef, useSubscribe } from '@/components';
import { createNextTickOnce, mergeState } from '@/utils';
import clsx from 'clsx';
import React, {
  forwardRef,
  useCallback,
  useContext,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { findDOMNode } from 'react-dom';
import styles from './index.less';
import { ConfigCtx, Context } from './shared';

function DroppableContainer({ children, className, ...props }, outerRef) {
  const {
    rowKey,

    containerComponent: Comp = 'div',
    containerClassName,
    containerDraggingClassName,
    droppableAreaClassName,
    overlayClassName,

    getDroppableKeys,
    onDrop,
    canDropOutside,
  } = useContext(ConfigCtx);

  const all = useRef(new Map()).current;
  const tbodyRef = useRef();
  const ref = useForkRef(tbodyRef, outerRef);
  const overlayRef = useRef();

  const [state, setState] = useState({
    dragging: false,
    startAt: null,
    transform: null,
    rowCtx: null,
  });
  const updateState = useRef((next) => setState((prev) => mergeState(prev, next))).current;
  const [currentIndex, setIndex] = useState(-1);
  const [mountedKeys, setMountedKeys] = useState([]);
  const [shouldUpdate, setShouldUpdate] = useState(false);
  const [droppableCtxList, setCtxList] = useState([]);

  const getBounds = useCallback(
    (nodeRef) => nodeRef.current && findDOMNode(nodeRef.current).getBoundingClientRect(),
    [],
  );
  const getRowKey = useCall((rowCtx) => rowCtx?.row?.[rowKey ?? 'id']);

  const mountedRef = useMountedRef();
  const updateMounted = useCallback(
    createNextTickOnce(() => {
      if (mountedRef.current) {
        setMountedKeys(Array.from(all).map((v) => v[0]));
        setShouldUpdate(false);
      }
    }),
    [],
  );

  const mountRow = useCall((rowCtx) => {
    all.set(getRowKey(rowCtx), rowCtx);
    updateMounted();
  });

  const unmountRow = useCall((rowCtx) => {
    all.delete(getRowKey(rowCtx));
    updateMounted();
  });

  useEffect(() => {
    if (state.rowCtx) {
      state.rowCtx.rowHeight = state.rowCtx.ref.current.offsetHeight;
    }
  }, [state.rowCtx]);

  const draggingRowKey = getRowKey(state.rowCtx);
  const draggingRowKeyRef = useEffectRef(draggingRowKey);

  useLayoutEffect(() => {
    if (draggingRowKey) {
      document.body.style.cursor = 'grabbing';

      (async (_ctxList) => {
        let ctxList = await _ctxList;

        if (!draggingRowKeyRef.current) return;

        ctxList = ctxList.map((key) => {
          const rowCtx = all.get(key);
          rowCtx.originRect = getBounds(rowCtx.ref);
          rowCtx.currentIndex = null;
          return rowCtx;
        });
        ctxList = ctxList.sort((a, b) => a.originRect?.y - b.originRect?.y);
        ctxList.forEach((rowCtx, index) => {
          rowCtx.index = index;
        });
        setCtxList(ctxList);
      })(getDroppableKeys?.(all, state.rowCtx) ?? Array.from(all.keys()));
    } else {
      document.body.style.cursor = null;
      setCtxList([]);
    }

    setShouldUpdate(false);
  }, [draggingRowKey]);

  // 计算拖放区域
  const [[top, areaHeight, left, areaWidth], setDropArea] = useState([0, 0, 0, 0]);
  useLayoutEffect(() => {
    if (!draggingRowKey) return;
    console.log('update drop area');
    setTimeout(setShouldUpdate, 0, true);
    const overlayRect = getBounds(overlayRef);
    let top, right, left, bottom;
    droppableCtxList.forEach(({ originRect }) => {
      const x = originRect.x - overlayRect.x;
      const y = originRect.y - overlayRect.y;
      const x2 = x + originRect.width;
      const y2 = y + originRect.height;
      left = Math.min(left ?? x, x);
      top = Math.min(top ?? y, y);
      right = Math.max(right ?? x2, x2);
      bottom = Math.max(bottom ?? y2, y2);
    });
    const width = right - left;
    const height = bottom - top;
    setDropArea([top, height, left, width]);
  }, [droppableCtxList, mountedKeys]);

  const onDrag = useCall((rowCtx, event) => {
    if (!state.dragging && ['mousemove'].includes(event.type)) return;
    if (!state.dragging) {
      const startAt = { x: event.clientX, y: event.clientY };
      const transform = { x: 0, y: 0 };
      rowCtx.dragStartRect = getBounds(rowCtx.ref);
      updateState({ dragging: true, startAt, transform, rowCtx });
    }
  });

  const dragCache = useRef([0, 0, false]);
  const onDragOver = useCall((event) => {
    if (!state.rowCtx || !shouldUpdate) return setIndex(-1);
    let { clientX, clientY } = event;
    let offsetX = state.rowCtx.dragStartRect.x - state.rowCtx.originRect.x;
    let offsetY = state.rowCtx.dragStartRect.y - state.rowCtx.originRect.y;

    if (clientX < 0 || clientY < 0 || clientX > window.innerWidth || clientY > window.innerHeight) {
      clientX = dragCache.current[0];
      clientY = dragCache.current[1];
      dragCache.current[2] = true;
    } else {
      dragCache.current = [clientX, clientY, false];
    }

    updateState({
      transform: {
        x: clientX - state.startAt.x + offsetX,
        y: clientY - state.startAt.y + offsetY,
      },
    });

    const currentRect = getBounds(state.rowCtx.ref);
    const currentKey = getRowKey(state.rowCtx);
    const currentBottom = currentRect.y + currentRect.height;
    const currentTop = currentRect.y;

    let lastCenterY;
    let lastIndex = -1;
    for (const [index, rowCtx] of droppableCtxList.entries()) {
      if (getRowKey(rowCtx) === currentKey) continue;
      const { x, y, width, height } = rowCtx.originRect;
      const bottom = y + height;
      const yCenter = bottom - Math.round(height / 2);
      lastCenterY = yCenter;
      if (currentBottom < yCenter) {
        lastIndex = index;
        break;
      }
      if (currentTop < yCenter) {
        lastIndex = index + 1;
        break;
      }
    }
    if (lastIndex === -1 && currentBottom > lastCenterY) lastIndex = droppableCtxList.length;
    lastIndex -= 1;
    setIndex(lastIndex);
    state.rowCtx.currentIndex = lastIndex;
  });

  const onDragEnd = useCall(() => {
    if (!state.dragging) return;
    if (!dragCache.current[2] || canDropOutside) {
      onDrop?.(state.rowCtx, currentIndex, all);
    }

    setIndex(-1);
    updateState({
      dragging: false,
      startAt: null,
      transform: null,
      rowCtx: null,
    });
  });

  const orderState = useMemo(() => {
    return shouldUpdate ? [state.rowCtx, currentIndex, droppableCtxList] : [null, -1, []];
  }, [shouldUpdate, currentIndex]);
  const subscribe = useSubscribe(state.rowCtx);
  const subTransform = useSubscribe(state.transform);
  const subOrder = useSubscribe(orderState);

  const context = useRef({
    mountRow,
    unmountRow,
    onDrag,
    onDragEnd,
    subscribe,
    subTransform,
    subOrder,
    getBounds,
    getRowKey,
  }).current;

  useEffect(() => {
    const events = [
      [onDragEnd, ['mouseup', 'touchend', 'pointerup']],
      [onDragOver, ['mousemove', 'touchmove', 'pointermove']],
    ];

    events.forEach(([handler, eventTypes]) => {
      eventTypes.forEach((eventType) => {
        document.addEventListener(eventType, handler, { passive: true });
      });
    });

    return () => {
      events.forEach(([handler, eventTypes]) => {
        eventTypes.forEach((eventType) => {
          document.removeEventListener(eventType, handler, { passive: true });
        });
      });
    };
  }, []);

  const clipPath = draggingRowKey
    ? `polygon(evenodd,
0% 0%,
${left}px ${top}px,
${left + areaWidth}px ${top}px,
${left + areaWidth}px ${top + areaHeight}px,
${left}px ${top + areaHeight}px,
${left}px ${top}px,
0% 0%, 100% 0%, 100% 100%, 0 100%)`
    : null;

  const klassName = clsx(
    className,
    styles.draggableArea,
    containerClassName,
    draggingRowKey && clsx(styles.dragging, containerDraggingClassName),
  );

  return (
    <Context.Provider value={context}>
      <Comp ref={ref} className={klassName} {...props}>
        {children}
        <div ref={overlayRef} className={clsx(styles.dropAreaOverlay, overlayClassName)} style={{ clipPath }} />
        <div
          className={clsx(styles.dropAreaBorder, droppableAreaClassName)}
          style={{ top: top + 2, height: areaHeight - 4, left: left + 2, width: areaWidth - 4 }}
        />
      </Comp>
    </Context.Provider>
  );
}

DroppableContainer = forwardRef(DroppableContainer);
export default DroppableContainer;
